package org.doubango.ngn.h264

import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.media.MediaCodec
import android.media.MediaCodecInfo
import android.media.MediaCodecList
import android.media.MediaFormat
import android.os.Build
import com.evendai.loglibrary.Timber
import org.doubango.ngn.media.NgnProxyVideoProducer
import java.nio.ByteBuffer


@TargetApi(Build.VERSION_CODES.LOLLIPOP)
class H264Encoder(producer: NgnProxyVideoProducer, val isPortrait: Boolean) {
    private val mMediaCodec: MediaCodec
    private var inputBufferIndex: Int = MediaCodec.INFO_TRY_AGAIN_LATER
    private var outputBufferIndex: Int = MediaCodec.INFO_TRY_AGAIN_LATER
    private lateinit var inputBuffer: ByteBuffer
    private lateinit var outputBuffer: ByteBuffer
    private lateinit var configOutputBytes: ByteArray
    @Suppress("SpellCheckingInspection")
    private lateinit var h264Datas: ByteArray
    @Suppress("SpellCheckingInspection")
    private lateinit var keyFrameDatas: ByteArray
    private var mBufferInfo = MediaCodec.BufferInfo()

    private var startTime = 0L
    private var receiveFrameCount = 0
    @Suppress("PrivatePropertyName")
    private val _1K = 1024
    @Suppress("PrivatePropertyName")
    private val _1M = _1K * 1024

    private val mH264ToMp4SaverThread = H264ToMp4SaverThread()
    private val mH264SenderThread = H264SenderThread()
    @Suppress("SpellCheckingInspection")
    private val mH264DoubangoSenderThread = H264DoubangoSenderThread(producer)

    init {
        val bitrate = getBitrate(producer.videoWidth, producer.videoHeight) /*producer.videoWidth * producer.videoHeight*/
        Timber.i("width = ${producer.videoWidth}, height = ${producer.videoHeight}, bitrate = ${bitrate / 1024f / 1024f}M")

        printParameter()

        mMediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC)
//        mMediaCodec = MediaCodec.createByCodecName("OMX.google.h264.encoder")
        // MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC) 这样获取的H264编码器为：OMX.qcom.video.encoder.avc
        // OMX.qcom.video.encoder.avc   是硬件编码器，支持NV12
        // OMX.google.h264.encoder      是软件编码器，支持NV12、I420

        // 当视频旋转90度或270度时，此处的宽高要调换位置
        val mediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC,
                if (isPortrait) producer.videoWidth else producer.videoHeight,
                if (isPortrait) producer.videoHeight else producer.videoWidth)

        /*
        球机的编码器支持的颜色格式：
        COLOR_FormatYUV420SemiPlanar（NV12）  十进制：21
        COLOR_FormatYUV420Planar（I420）      十进制：19
        上面两种是过时的，都推荐用：COLOR_FormatYUV420Flexible，明明是两种不同格式，不知道为什么推荐用的是同一个
        COLOR_FormatYUV420Flexible  十进制：2135033992，十六进制：7F420888
        COLOR_FormatSurface  十进制2130708361，十六进制： 7F000789
        */
//        mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, if (NgnApplication.isBall()) MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible else MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar)
        @Suppress("DEPRECATION")
        mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar)

        // 注：不能使用静态码率，否则视频画面会有不停的间隔地出现微模糊效果。虽然使用了动态码率，但是码率值还是要设置的，如果不设置在调用mMediaCodec.configure()时会报异常
        // 动态码率：真实码率会是我们设置的码率的上下范围浮动,码率会根据视频画面变化而实时变化。
        mediaFormat.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR) // 设置码率为动态码率，默认也是动态码率
        mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate) // 码率（即比特率）
        mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 25)      // 帧速（25帧/秒）
        mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2) // I帧间隔（1帧/2秒），因为帧 1秒出25帧，2秒就出50帧，所以I帧间隔为2的话就是每50帧出一个关键帧
        //MediaFormat.KEY_ROTATION: 描述输出到output surface上所需的顺时针旋转的键。 仅当使用output surface配置编解码器时才使用此键。
        // 关联值为整数，代表度。支持的值是0、90、180或270。如果未指定，则旋转默认为0。
        //mediaFormat.setInteger(MediaFormat.KEY_ROTATION, 90)

        // 第二个参数用于显示解码器的视频内容，第三个参数为编解码器的解密参数，第四个参数为指定为编码器
        mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)

        mH264DoubangoSenderThread.start()
        mH264SenderThread.start()
        mH264ToMp4SaverThread.start()
        mMediaCodec.start()
        Timber.e("codeName = ${mMediaCodec.codecInfo.name}")
    }

    @Suppress("DEPRECATION")
    private fun printParameter() {
        val codecCount = MediaCodecList.getCodecCount()
        var codecInfo: MediaCodecInfo?
        for (i in 0 until codecCount) {
            codecInfo = MediaCodecList.getCodecInfoAt(i)
            if (codecInfo?.isEncoder != true) {
                continue
            }
            var found = false
            Timber.e("----------------------")
            codecInfo.supportedTypes.forEach { type ->
                if (type == "video/avc") {
                    Timber.e("找到编码器： ${codecInfo.name}")
                    found = true
                }
            }
            if (found) {
                val capabilities = codecInfo.getCapabilitiesForType("video/avc")
                capabilities.colorFormats.forEach { colorFormat ->
                    Timber.e( "支持的颜色格式： $colorFormat")
                }
            }
        }
    }

    /**
     * 把NV12格式的Yuv数据编码为H264数据，此方法执行完就是编码完一帧的数据
     */
    @SuppressLint("SwitchIntDef")
    fun encodeYuvToH264(@Suppress("SpellCheckingInspection") yuvDatas: ByteArray) {
        // 把YUV数据放入MediaCodec中进行H264编码
        inputBufferIndex = mMediaCodec.dequeueInputBuffer(10_000) // 如果10毫秒都等不到可用缓冲，则这一帧的yuv数据将丢掉。谷歌官方Demo也是用的这个值
        @Suppress("SpellCheckingInspection")
        if (inputBufferIndex >= 0) {
            inputBuffer = mMediaCodec.getInputBuffer(inputBufferIndex)!!
            inputBuffer.put(yuvDatas)
            // if (!isRuning) flag = MediaCodec.BUFFER_FLAG_END_OF_STREAM;
            // 参数presentationTimeUs：指定我们这一帧数据应该在什么时间显示，单位为微秒，System.nanoTime()得到是当前时间的表示的纳秒。1000纳秒等于1微秒
            // 谷歌官方Demo计算方式为： presentationTimeUs = frameIndex * 1000000(1秒对应的微秒值） / fps。frameIndex从0开始
            // 比如fps为10帧/秒，则第1帧应该在0.0秒时显示，第2帧应该在0.1秒时显示，第3帧应该在0.2秒时显示，当然这些都要换成参数需要的微秒单位
            // 我发现presentationTimeUs不需要从0开始，只要把接收到帧数据时的当前时间传给它即可
            mMediaCodec.queueInputBuffer(inputBufferIndex, 0, yuvDatas.size, System.nanoTime() / 1000, 0) // int bufferFlagEndOfStream = MediaCodec.BUFFER_FLAG_END_OF_STREAM，当要结束时使用这个Flag
        }

        // 如果已经没有数据了，可以调用mMediaCodec.signalEndOfInputStream()来发送一个达到流结尾的信号，但是不调用也没有任何影响

        // 从MediaCodec中取出编好的H264数据并使用（如保存、发送）
        outputBufferIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, 10_000)
//        Timber.i("outputBufferIndex = $outputBufferIndex")
        while (outputBufferIndex >= 0) {
            /* 索引为负数时有这些值：
                    MediaCodec.INFO_TRY_AGAIN_LATER：-1       // 尚无可用输出，稍后再试（即在mMediaCodec.dequeueOutputBuffer中指定了超时，而且这个超时时间到了还没有可用输出则会返回这个值，
                                                                 此时不应该再循环取缓冲输出,因为已经没有数据可取了，应该跳出循环，开始等待新数据的输入）
                    MediaCodec.INFO_OUTPUT_FORMAT_CHANGED：-2 // 应该在接收缓冲区之前发生（也就是还没有进行编码工作之前发生），并且应该只发生一次。输出格式已更改，后续数据将采用新格式。 getOutputFormat（）返回新格式。
                                                                请注意，您还可以使用新的getOutputFormat（int）方法来获取特定输出缓冲区的格式。这使您不必跟踪输出格式更改。
                    MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED：-3 // 表示输出的缓冲区已经改变了，需要使用getOutputBuffers()来获取新的缓冲区，
                                                                此外，此事件表示视频缩放模式可能已重置为默认值。而这个函数在Api21中已经弃用，
                                                                所以我们在使用新的方式获取缓冲数据时可以忽略这个返回值的情况。
                                                                现在我们是使用getOutputBuffer(index)来获取缓冲输出，而不是用getOutputBuffers()来获取缓冲区
             */
            outputBuffer = mMediaCodec.getOutputBuffer(outputBufferIndex)!!
            when (mBufferInfo.flags) {
                0, MediaCodec.BUFFER_FLAG_KEY_FRAME -> { // 普通帧、关键帧
                    h264Datas = ByteArray(mBufferInfo.size)
                    outputBuffer.get(h264Datas)
                }
                MediaCodec.BUFFER_FLAG_CODEC_CONFIG -> { // 配置数据，这个是配置的数据，包含了视频大小、帧速等信息，这个数据大小为30byte
                                                         // 在输出真正的视频数据之前会先输出这个配置数据，而且只会输出一次。关于这个配置数据，其实在MediaFormat中包含有，
                                                         // 所以不需要把这个数据传MediaMuxer
                    Timber.i("得到配置数据，大小为：${mBufferInfo.size}")
                    configOutputBytes = ByteArray(mBufferInfo.size)
                    outputBuffer.get(configOutputBytes)
                }
                MediaCodec.BUFFER_FLAG_END_OF_STREAM -> Timber.i("编码结束") // 编码结束
            }


            @Suppress("SpellCheckingInspection")
            if (mBufferInfo.flags != MediaCodec.BUFFER_FLAG_CODEC_CONFIG) {
                if (mBufferInfo.flags == MediaCodec.BUFFER_FLAG_KEY_FRAME) {
                    // 网络传输时，关键帧需要在前面加上配置数据再发送
                    keyFrameDatas = ByteArray(configOutputBytes.size + h264Datas.size)
                    System.arraycopy(configOutputBytes, 0, keyFrameDatas, 0, configOutputBytes.size)
                    System.arraycopy(h264Datas, 0, keyFrameDatas, configOutputBytes.size, h264Datas.size)
                    mH264DoubangoSenderThread.addH264Datas(keyFrameDatas)
                    mH264SenderThread.addH264Datas(keyFrameDatas)  // 发送到ffmpeg服务器
                } else {
                    // 网络传输时，普通帧只直发送
                    mH264DoubangoSenderThread.addH264Datas(h264Datas)
                    mH264SenderThread.addH264Datas(h264Datas)  // 发送到ffmpeg服务器
                }

                // 保存mp4时，不要在关键前面插入配置数据，否则会出现花屏
                // 注：这里拿到的MediaFormat才是有效的，才可以给MediaMuxer使用
                mH264ToMp4SaverThread.addH264Data(H264Data(h264Datas, mBufferInfo, mMediaCodec.getOutputFormat(outputBufferIndex)))
            }

            mMediaCodec.releaseOutputBuffer(outputBufferIndex, false) // 把缓存对象还给MediaCodec
            outputBufferIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, 10_000)

            if (mBufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
                Timber.i("已经到达流的终点了")
                break // 退出循环，无需再去获取编码的数据，肯定没有数据了。
            }
        }

        receiveFrameCount++
        if (System.currentTimeMillis() - startTime >= 10_000) {
            Timber.e("H264Encoder每10秒编码${receiveFrameCount}帧")
            startTime = System.currentTimeMillis()
            receiveFrameCount = 0
        }
    }

    private fun getBitrate(videoWidth: Int, videoHeight: Int): Int = when {
        /*(videoWidth == 1920 && videoHeight == 1080) || (videoWidth == 1080 && videoHeight == 1920) -> _1M * 3 shr 1
        (videoWidth == 1280 && videoHeight == 720) || (videoWidth == 720 && videoHeight == 1280) -> _1M
        (videoWidth == 640 && videoHeight == 480) || (videoWidth == 480 && videoHeight == 640) -> _1K * 500
        (videoWidth == 352 && videoHeight == 288) || (videoWidth == 288 && videoHeight == 352) -> _1K * 300
        else -> _1M * 1*/
        (videoWidth == 1920 && videoHeight == 1080) || (videoWidth == 1080 && videoHeight == 1920) -> _1M * 3 shr 1
        (videoWidth == 1280 && videoHeight == 720) || (videoWidth == 720 && videoHeight == 1280) -> _1M * 2
        (videoWidth == 640 && videoHeight == 480) || (videoWidth == 480 && videoHeight == 640) -> _1M
        (videoWidth == 352 && videoHeight == 288) || (videoWidth == 288 && videoHeight == 352) -> _1K * 500
        else -> _1M * 1
    }

    fun close() {
        try {
            mH264DoubangoSenderThread.close()
            mH264SenderThread.close()
            mH264ToMp4SaverThread.close()
            mMediaCodec.stop()
            mMediaCodec.release()
        } catch (e: Exception) {
            Timber.e(e,"，别慌，正常关闭${TAG}时出现的异常！")
        }
    }

    companion object {
        private val TAG = H264Encoder::class.java.simpleName
//        private val BIT_RATE: Int = 0 // videoWidth * videoHeight * 3 * 8 * FRAME_RATE / COMPRESS_RATIO;
//        private val COMPRESS_RATIO = 256
    }
}
